import type { AObject } from "./AObject";
import type { Agent } from "./Agent";
import type { Character } from "./Character";
import { RoutineResult, type ScheduledRoutine, type ActionHandler, type ActionReportArguments, RoutineResultType } from "./Definitions";
import { HistoryEntry } from "./NarrativeBase";
import type { TypedScheduledProcess } from "./WorldBase";
import * as Debug from "./Debug";

export type RoutineArguments = {
    object?: AObject,
    container?: AObject,
    instrument?: AObject,
    state?: boolean
};

export type RoutineParameters = {
    [name in keyof RoutineArguments]: 
        [condition: (obj: RoutineArguments[name], args: RoutineArguments) => boolean,
         except: (obj: RoutineArguments[name], args: RoutineArguments) => RoutineResult][]
};

type VoidableScheduledRoutine = TypedScheduledProcess<RoutineResult | void>;

export class RoutineSpecification<args extends unknown[]> {
    parameters: RoutineParameters = {};
    parts: ((self: RoutineInstance, ...args: args) => VoidableScheduledRoutine | null)[] = [];
    doFatigeTime?: (self: RoutineInstance, ...args: args) => [number, number];
    createReport?: (self: RoutineInstance, ...args: args) => HistoryEntry;
    #threshold = 0.05;

    constructor() {};

    param<K extends keyof RoutineArguments>(name: K,
        ...conds: [c: (obj: RoutineArguments[K], args: RoutineArguments) => boolean, 
            e: (obj: RoutineArguments[K], args: RoutineArguments) => RoutineResult][]) 
    {
        let list: RoutineParameters[K] = [];
        Debug.assert(list != undefined);
        for (const [c, e] of conds)
            list.push([c, e]);
        this.parameters[name] = list;
        return this;
    }

    checkParams(how: (self: RoutineInstance, ...args: args) => RoutineArguments) {
        return this.then((self, ...args) => 
            self.routine.checkParameters(how(self, ...args)));
    }

    check(
        condition: (self: RoutineInstance, ...args: args) => boolean,
        except: (self: RoutineInstance, ...args: args) => RoutineResult)
    {
        this.parts.push(function*(self, ...args) {
            if (!condition(self, ...args))
                return except(self, ...args);
            return RoutineResult.Continue();
        });
        return this;
    }

    doIf(
        condition: (self: RoutineInstance, ...args: args) => boolean,
        what?: (self: RoutineInstance, ...args: args) => ScheduledRoutine,
        doubleCheck?: (self: RoutineInstance, ...args: args) => RoutineResult)
    {
        this.parts.push(function*(self, ...args) {
            if (condition(self, ...args)) {
                if (what) {
                    let res = self.check(yield* what(self, ...args));
                    if (res) return res;
                }
                if (doubleCheck) {
                    let res = doubleCheck(self, ...args);
                    if (res) return res;
                }
            }
            return RoutineResult.Continue();
        });
        return this;
    }

    tryHandler(how: (self: RoutineInstance, ...args: args) 
        => [AObject, ActionHandler<AObject> | undefined]) 
    {
        this.parts.push(function*(self, ...args) {
            console.log('trying: ', how);
            let [obj, handler] = how(self, ...args);
            return handler == undefined 
                ? RoutineResult.Continue() 
                : yield* handler(obj, self.agent, self);
        });
        return this;
    }

    tryRoutine(how: (self: RoutineInstance, ...args: args) 
        => [AObject, ARoutine<[self: AObject]> | undefined])
    {
        this.parts.push(function*(self, ...args) {
            console.log('trying: ', how);
            let [obj, handler] = how(self, ...args);
            return handler == undefined 
                ? RoutineResult.Continue() 
                : yield* handler.execDirect(self.agent, obj);
        });
        return this;
    }

    ensureNotHolding() {
        this.parts.push(function*(self, ..._) {
            if (self.agent.holdingItem != null)
                return RoutineResult.CantDoWhileHoldingItem({}, true);
            return RoutineResult.Continue();
        });
        return this;
    }

    fatigueAndTime(
        fatime: ((self: RoutineInstance, ...args: args) => [number, number]) | [number, number], 
        threshold: number = 0.05) 
    {
        this.doFatigeTime = Array.isArray(fatime) ? () => fatime : fatime;
        this.#threshold = threshold;
        return this;
    }

    requireTime(
        time: ((self: RoutineInstance, ...args: args) => number) | number) 
    {
        this.parts.push(function*(self, ...args) {
            let res = yield* RoutineUtility.requireTime(
                typeof time == 'number' ? time : time(self, ...args), self.character);
            return res;
        });
        return this;
    }

    report(what: (self: RoutineInstance, ...args: args) 
        => ActionReportArguments | HistoryEntry) 
    {
        this.createReport = (self, ...args) => {
            let out = what(self, ...args);
            return out instanceof HistoryEntry ? out : self.character.reportAction(out);
        };
        return this;
    }

    do(what: (self: RoutineInstance, ...args: args) => VoidableScheduledRoutine | null) {
        this.parts.push((self, ...args) => what(self, ...args));
        return this;
    }

    then(what: (self: RoutineInstance, ...args: args) => RoutineResult | void | null) {
        this.parts.push(function*(self, ...args) {
            return what(self, ...args) ?? RoutineResult.Continue() });
        return this;
    }

    effect(
        what: (self: RoutineInstance, ...args: args) => RoutineResult | void | null) 
    {
        this.main(function*(self, ...args) {
            return what(self, ...args) ?? RoutineResult.Continue() });
        return this;
    }

    main(what: (self: RoutineInstance, ...args: args) => VoidableScheduledRoutine) {
        let _this = this;
        this.parts.push(function*(self, ...args) {
            let [fa, ti] = [0, 0];
            let report: HistoryEntry | null = null;
            if (_this.doFatigeTime) {
                [fa, ti] = _this.doFatigeTime(self, ...args);
                let res = self.check(
                    RoutineUtility.checkFatigue(self.agent, fa, _this.#threshold));
                if (res) return res;
            }
            if (_this.createReport) report = 
                _this.createReport(self, ...args).markImplicit(self.isSilent);
            report?.commence();
            if (_this.doFatigeTime) {
                // ADV_TODO: handle interrupted report
                let res = self.check(
                    yield* RoutineUtility.requireTime(ti, self.character));
                if (res) {
                    report?.interrupt();
                    return res;
                }
            }
            self.agent.strength -= fa;
            report?.cease();
            // U.annotationText(self.agent, `[costing ${(fa * 100).toFixed(2)}% strength]`);
            let res = self.check((yield* what(self, ...args)) ?? null);
            report?.markImplicit(self.isSilent);
            return res ?? undefined;
        });
        return this;
    }

    ok() {
        return new ARoutine<args>(this);
    }
}

export interface RoutineBase {
    checkParameters(rargs: RoutineArguments): RoutineResult | null;
    checkParameter(rargs: RoutineArguments, name: keyof RoutineArguments): RoutineResult | null;
}

export class RoutineInstance {
    report: HistoryEntry | null = null;
    isSilent = false;
    isDirect = true;

    get character() {        
        Debug.assert(this.agent.character !== null);
        return this.agent.character;
    }

    constructor(
        public readonly routine: RoutineBase,
        public readonly agent: Agent) {
    }

    check(ret?: RoutineResult | null): RoutineResult | null {
        if (!ret) return null;
        if (ret.type == RoutineResultType.ContinueSilent)
            this.isSilent = true;
        else if (ret.type != RoutineResultType.Continue)
            return ret;
        return null;
    }
}

// ADV_NOTE: should we separate it to ARoutine and RoutineInstance?
export class ARoutine<args extends any[]> implements RoutineBase {
    readonly specs: RoutineSpecification<args>;

    constructor(specs: RoutineSpecification<args>) {
        this.specs = specs;
    }

    checkParameters(rargs: RoutineArguments) {
        for (let param in this.specs.parameters) {
            let name = param as keyof RoutineArguments;
            let res = this.checkParameter(rargs, name);
            if (res) return res;
        }
        return null;
    }

    checkParameter(rargs: RoutineArguments, name: keyof RoutineArguments) {
        let param = this.specs.parameters[name];
        if (!param) return null; //throw new Error('no such param: ' + name);
        for (let [cond, except] of param)
            if (!cond(rargs[name] as any, rargs))
                return except(rargs[name] as any, rargs);
        return null;
    }

    *exec(ag: Agent, silent: boolean, direct: boolean, ...args: args): ScheduledRoutine {
        const instance = new RoutineInstance(this, ag);
        instance.isSilent = silent;
        instance.isDirect = direct;
        let ret: RoutineResult | null = null;
        for (const part of this.specs.parts) {
            let v = part(instance, ...args);
            if (!v) continue;
            ret = instance.check((yield* v) ?? null);
            if (ret) {
                console.log('quitting at', v, '-', RoutineResultType[ret.type]);
                return ret;
            }
        }
        console.log('quitting ok');
        return RoutineResult.Ok();
    }

    execIndirect(ag: Agent, silent: boolean, ...args: args) {
        return this.exec(ag, silent, false, ...args);
    }

    execDirect(ag: Agent, ...args: args) {
        return this.exec(ag, false, true, ...args);
    }
}

export const RoutineUtility = {
    *requireTime(t: number, char: Character) {
        if (!(yield* char.autoPassTime(t)))
            return RoutineResult.Interrupted({}, true);
        return RoutineResult.Continue();
    },

    checkFatigue(agent: Agent, fatigue: number, threshold = 0.05) {
        if (agent.strength - fatigue < threshold) {
            return agent.strength < threshold 
                ? RoutineResult.NoStrength({}, true) 
                : RoutineResult.NotEnoughStrength({}, true);
        }
        return RoutineResult.Continue();
    },

    ensureNotHolding(agent: Agent) {
        if (agent.holdingItem != null)
            return RoutineResult.CantDoWhileHoldingItem({}, true);
        return RoutineResult.Continue();
    },

    *ensureOwns(obj: AObject, agent: Agent) {
        if (obj.parentContainer != agent) {
            let res = yield* agent.takeObj.execIndirect(agent, false, obj);
            if (res.type) return res;
            if (obj.parentContainer != agent) return RoutineResult.Bug();
        }
        return RoutineResult.Continue();
    },

    *ensureIsOpen(cont: AObject, agent: Agent) {
        if (cont.isOpenable && !cont.openState) {
            let res = yield* agent.openClose.execIndirect(agent, false, cont, true);
            if (res.type) return res;
            if (!cont.openState) return RoutineResult.CantDoWhileNotOpen({object: cont});
        }
        return RoutineResult.Continue();
    }
}
